home *** CD-ROM | disk | FTP | other *** search
/ PC go! 2008 October / PCgo 2008-10 (DVD).iso / interface / contents / vollversionen_6617 / 21733 / files / xulrunner / modules / DownloadUtils.jsm < prev    next >
Encoding:
Text File  |  2008-08-20  |  17.0 KB  |  514 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is Download Manager Utility Code.
  15.  *
  16.  * The Initial Developer of the Original Code is
  17.  * Edward Lee <edward.lee@engineering.uiuc.edu>.
  18.  * Portions created by the Initial Developer are Copyright (C) 2008
  19.  * the Initial Developer. All Rights Reserved.
  20.  *
  21.  * Contributor(s):
  22.  *
  23.  * Alternatively, the contents of this file may be used under the terms of
  24.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  25.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  26.  * in which case the provisions of the GPL or the LGPL are applicable instead
  27.  * of those above. If you wish to allow use of your version of this file only
  28.  * under the terms of either the GPL or the LGPL, and not to allow others to
  29.  * use your version of this file under the terms of the MPL, indicate your
  30.  * decision by deleting the provisions above and replace them with the notice
  31.  * and other provisions required by the GPL or the LGPL. If you do not delete
  32.  * the provisions above, a recipient may use your version of this file under
  33.  * the terms of any one of the MPL, the GPL or the LGPL.
  34.  *
  35.  * ***** END LICENSE BLOCK ***** */
  36.  
  37. var EXPORTED_SYMBOLS = [ "DownloadUtils" ];
  38.  
  39. /**
  40.  * This module provides the DownloadUtils object which contains useful methods
  41.  * for downloads such as displaying file sizes, transfer times, and download
  42.  * locations.
  43.  *
  44.  * List of methods:
  45.  *
  46.  * [string status, double newLast]
  47.  * getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes,
  48.  *                   [optional] double aSpeed, [optional] double aLastSec)
  49.  *
  50.  * string progress
  51.  * getTransferTotal(int aCurrBytes, [optional] int aMaxBytes)
  52.  *
  53.  * [string timeLeft, double newLast]
  54.  * getTimeLeft(double aSeconds, [optional] double aLastSec)
  55.  *
  56.  * [string displayHost, string fullHost]
  57.  * getURIHost(string aURIString)
  58.  *
  59.  * [double convertedBytes, string units]
  60.  * convertByteUnits(int aBytes)
  61.  *
  62.  * [int time, string units, int subTime, string subUnits]
  63.  * convertTimeUnits(double aSecs)
  64.  */
  65.  
  66. const Cc = Components.classes;
  67. const Ci = Components.interfaces;
  68. const Cu = Components.utils
  69. Cu.import("resource://gre/modules/PluralForm.jsm");
  70.  
  71. const kDownloadProperties =
  72.   "chrome://mozapps/locale/downloads/downloads.properties";
  73.  
  74. // These strings will be converted to the corresponding ones from the string
  75. // bundle on use
  76. let kStrings = {
  77.   statusFormat: "statusFormat2",
  78.   transferSameUnits: "transferSameUnits",
  79.   transferDiffUnits: "transferDiffUnits",
  80.   transferNoTotal: "transferNoTotal",
  81.   timePair: "timePair",
  82.   timeLeftSingle: "timeLeftSingle",
  83.   timeLeftDouble: "timeLeftDouble",
  84.   timeFewSeconds: "timeFewSeconds",
  85.   timeUnknown: "timeUnknown",
  86.   doneScheme: "doneScheme",
  87.   doneFileScheme: "doneFileScheme",
  88.   units: ["bytes", "kilobyte", "megabyte", "gigabyte"],
  89.   // Update timeSize in convertTimeUnits if changing the length of this array
  90.   timeUnits: ["seconds", "minutes", "hours", "days"],
  91. };
  92.  
  93. // This object will lazily load the strings defined in kStrings
  94. let gStr = {
  95.   /**
  96.    * Initialize lazy string getters
  97.    */
  98.   _init: function()
  99.   {
  100.     // Make each "name" a lazy-loading string that knows how to load itself. We
  101.     // need to locally scope name and value to keep them around for the getter.
  102.     for (let [name, value] in Iterator(kStrings))
  103.       let ([n, v] = [name, value])
  104.         gStr.__defineGetter__(n, function() gStr._getStr(n, v));
  105.   },
  106.  
  107.   /**
  108.    * Convert strings to those in the string bundle. This lazily loads the
  109.    * string bundle *once* only when used the first time.
  110.    */
  111.   get _getStr()
  112.   {
  113.     // Delete the getter to be overwritten
  114.     delete gStr._getStr;
  115.  
  116.     // Lazily load the bundle into the closure on first call to _getStr
  117.     let getStr = Cc["@mozilla.org/intl/stringbundle;1"].
  118.                  getService(Ci.nsIStringBundleService).
  119.                  createBundle(kDownloadProperties).
  120.                  GetStringFromName;
  121.  
  122.     // _getStr is a function that sets string "name" to stringbundle's "value"
  123.     return gStr._getStr = function(name, value) {
  124.       // Delete the getter to be overwritten
  125.       delete gStr[name];
  126.  
  127.       try {
  128.         // "name" is a string or array of the stringbundle-loaded "value"
  129.         return gStr[name] = typeof value == "string" ?
  130.                             getStr(value) :
  131.                             value.map(getStr);
  132.       } catch (e) {
  133.         log(["Couldn't get string '", name, "' from property '", value, "'"]);
  134.         // Don't return anything (undefined), and because we deleted ourselves,
  135.         // future accesses will also be undefined
  136.       }
  137.     };
  138.   },
  139. };
  140. // Initialize the lazy string getters!
  141. gStr._init();
  142.  
  143. // Keep track of at most this many second/lastSec pairs so that multiple calls
  144. // to getTimeLeft produce the same time left
  145. const kCachedLastMaxSize = 10;
  146. let gCachedLast = [];
  147.  
  148. let DownloadUtils = {
  149.   /**
  150.    * Generate a full status string for a download given its current progress,
  151.    * total size, speed, last time remaining
  152.    *
  153.    * @param aCurrBytes
  154.    *        Number of bytes transferred so far
  155.    * @param [optional] aMaxBytes
  156.    *        Total number of bytes or -1 for unknown
  157.    * @param [optional] aSpeed
  158.    *        Current transfer rate in bytes/sec or -1 for unknown
  159.    * @param [optional] aLastSec
  160.    *        Last time remaining in seconds or Infinity for unknown
  161.    * @return A pair: [download status text, new value of "last seconds"]
  162.    */
  163.   getDownloadStatus: function(aCurrBytes, aMaxBytes, aSpeed, aLastSec)
  164.   {
  165.     if (isNil(aMaxBytes))
  166.       aMaxBytes = -1;
  167.     if (isNil(aSpeed))
  168.       aSpeed = -1;
  169.     if (isNil(aLastSec))
  170.       aLastSec = Infinity;
  171.  
  172.     // Calculate the time remaining if we have valid values
  173.     let seconds = (aSpeed > 0) && (aMaxBytes > 0) ?
  174.       (aMaxBytes - aCurrBytes) / aSpeed : -1;
  175.  
  176.     // Update the bytes transferred and bytes total
  177.     let status;
  178.     let (transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes)) {
  179.       // Insert 1 is the download progress
  180.       status = replaceInsert(gStr.statusFormat, 1, transfer);
  181.     }
  182.  
  183.     // Update the download rate
  184.     let ([rate, unit] = DownloadUtils.convertByteUnits(aSpeed)) {
  185.       // Insert 2 is the download rate
  186.       status = replaceInsert(status, 2, rate);
  187.       // Insert 3 is the |unit|/sec
  188.       status = replaceInsert(status, 3, unit);
  189.     }
  190.  
  191.     // Update time remaining
  192.     let ([timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec)) {
  193.       // Insert 4 is the time remaining
  194.       status = replaceInsert(status, 4, timeLeft);
  195.  
  196.       return [status, newLast];
  197.     }
  198.   },
  199.  
  200.   /**
  201.    * Generate the transfer progress string to show the current and total byte
  202.    * size. Byte units will be as large as possible and the same units for
  203.    * current and max will be supressed for the former.
  204.    *
  205.    * @param aCurrBytes
  206.    *        Number of bytes transferred so far
  207.    * @param [optional] aMaxBytes
  208.    *        Total number of bytes or -1 for unknown
  209.    * @return The transfer progress text
  210.    */
  211.   getTransferTotal: function(aCurrBytes, aMaxBytes)
  212.   {
  213.     if (isNil(aMaxBytes))
  214.       aMaxBytes = -1;
  215.  
  216.     let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes);
  217.     let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes);
  218.  
  219.     // Figure out which byte progress string to display
  220.     let transfer;
  221.     if (total < 0)
  222.       transfer = gStr.transferNoTotal;
  223.     else if (progressUnits == totalUnits)
  224.       transfer = gStr.transferSameUnits;
  225.     else
  226.       transfer = gStr.transferDiffUnits;
  227.  
  228.     transfer = replaceInsert(transfer, 1, progress);
  229.     transfer = replaceInsert(transfer, 2, progressUnits);
  230.     transfer = replaceInsert(transfer, 3, total);
  231.     transfer = replaceInsert(transfer, 4, totalUnits);
  232.  
  233.     return transfer;
  234.   },
  235.  
  236.   /**
  237.    * Generate a "time left" string given an estimate on the time left and the
  238.    * last time. The extra time is used to give a better estimate on the time to
  239.    * show. Both the time values are doubles instead of integers to help get
  240.    * sub-second accuracy for current and future estimates.
  241.    *
  242.    * @param aSeconds
  243.    *        Current estimate on number of seconds left for the download
  244.    * @param [optional] aLastSec
  245.    *        Last time remaining in seconds or Infinity for unknown
  246.    * @return A pair: [time left text, new value of "last seconds"]
  247.    */
  248.   getTimeLeft: function(aSeconds, aLastSec)
  249.   {
  250.     if (isNil(aLastSec))
  251.       aLastSec = Infinity;
  252.  
  253.     if (aSeconds < 0)
  254.       return [gStr.timeUnknown, aLastSec];
  255.  
  256.     // Try to find a cached lastSec for the given second
  257.     aLastSec = gCachedLast.reduce(function(aResult, aItem)
  258.       aItem[0] == aSeconds ? aItem[1] : aResult, aLastSec);
  259.  
  260.     // Add the current second/lastSec pair unless we have too many
  261.     gCachedLast.push([aSeconds, aLastSec]);
  262.     if (gCachedLast.length > kCachedLastMaxSize)
  263.       gCachedLast.shift();
  264.  
  265.     // Apply smoothing only if the new time isn't a huge change -- e.g., if the
  266.     // new time is more than half the previous time; this is useful for
  267.     // downloads that start/resume slowly
  268.     if (aSeconds > aLastSec / 2) {
  269.       // Apply hysteresis to favor downward over upward swings
  270.       // 30% of down and 10% of up (exponential smoothing)
  271.       let (diff = aSeconds - aLastSec) {
  272.         aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff;
  273.       }
  274.  
  275.       // If the new time is similar, reuse something close to the last seconds,
  276.       // but subtract a little to provide forward progress
  277.       let diff = aSeconds - aLastSec;
  278.       let diffPct = diff / aLastSec * 100;
  279.       if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5)
  280.         aSeconds = aLastSec - (diff < 0 ? .4 : .2);
  281.     }
  282.  
  283.     // Decide what text to show for the time
  284.     let timeLeft;
  285.     if (aSeconds < 4) {
  286.       // Be friendly in the last few seconds
  287.       timeLeft = gStr.timeFewSeconds;
  288.     } else {
  289.       // Convert the seconds into its two largest units to display
  290.       let [time1, unit1, time2, unit2] =
  291.         DownloadUtils.convertTimeUnits(aSeconds);
  292.  
  293.       let pair1 = replaceInsert(gStr.timePair, 1, time1);
  294.       pair1 = replaceInsert(pair1, 2, unit1);
  295.       let pair2 = replaceInsert(gStr.timePair, 1, time2);
  296.       pair2 = replaceInsert(pair2, 2, unit2);
  297.  
  298.       // Only show minutes for under 1 hour or the second pair is 0
  299.       if (aSeconds < 3600 || time2 == 0) {
  300.         timeLeft = replaceInsert(gStr.timeLeftSingle, 1, pair1);
  301.       } else {
  302.         // We've got 2 pairs of times to display
  303.         timeLeft = replaceInsert(gStr.timeLeftDouble, 1, pair1);
  304.         timeLeft = replaceInsert(timeLeft, 2, pair2);
  305.       }
  306.     }
  307.  
  308.     return [timeLeft, aSeconds];
  309.   },
  310.  
  311.   /**
  312.    * Get the appropriate display host string for a URI string depending on if
  313.    * the URI has an eTLD + 1, is an IP address, a local file, or other protocol
  314.    *
  315.    * @param aURIString
  316.    *        The URI string to try getting an eTLD + 1, etc.
  317.    * @return A pair: [display host for the URI string, full host name]
  318.    */
  319.   getURIHost: function(aURIString)
  320.   {
  321.     let ioService = Cc["@mozilla.org/network/io-service;1"].
  322.                     getService(Ci.nsIIOService);
  323.     let eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
  324.                       getService(Ci.nsIEffectiveTLDService);
  325.     let idnService = Cc["@mozilla.org/network/idn-service;1"].
  326.                      getService(Ci.nsIIDNService);
  327.  
  328.     // Get a URI that knows about its components
  329.     let uri = ioService.newURI(aURIString, null, null);
  330.  
  331.     // Get the inner-most uri for schemes like jar:
  332.     if (uri instanceof Ci.nsINestedURI)
  333.       uri = uri.innermostURI;
  334.  
  335.     let fullHost;
  336.     try {
  337.       // Get the full host name; some special URIs fail (data: jar:)
  338.       fullHost = uri.host;
  339.     } catch (e) {
  340.       fullHost = "";
  341.     }
  342.  
  343.     let displayHost;
  344.     try {
  345.       // This might fail if it's an IP address or doesn't have more than 1 part
  346.       let baseDomain = eTLDService.getBaseDomain(uri);
  347.  
  348.       // Convert base domain for display; ignore the isAscii out param
  349.       displayHost = idnService.convertToDisplayIDN(baseDomain, {});
  350.     } catch (e) {
  351.       // Default to the host name
  352.       displayHost = fullHost;
  353.     }
  354.  
  355.     // Check if we need to show something else for the host
  356.     if (uri.scheme == "file") {
  357.       // Display special text for file protocol
  358.       displayHost = gStr.doneFileScheme;
  359.       fullHost = displayHost;
  360.     } else if (displayHost.length == 0) {
  361.       // Got nothing; show the scheme (data: about: moz-icon:)
  362.       displayHost = replaceInsert(gStr.doneScheme, 1, uri.scheme);
  363.       fullHost = displayHost;
  364.     } else if (uri.port != -1) {
  365.       // Tack on the port if it's not the default port
  366.       let port = ":" + uri.port;
  367.       displayHost += port;
  368.       fullHost += port;
  369.     }
  370.  
  371.     return [displayHost, fullHost];
  372.   },
  373.  
  374.   /**
  375.    * Converts a number of bytes to the appropriate unit that results in a
  376.    * number that needs fewer than 4 digits
  377.    *
  378.    * @param aBytes
  379.    *        Number of bytes to convert
  380.    * @return A pair: [new value with 3 sig. figs., its unit]
  381.    */
  382.   convertByteUnits: function(aBytes)
  383.   {
  384.     let unitIndex = 0;
  385.  
  386.     // Convert to next unit if it needs 4 digits (after rounding), but only if
  387.     // we know the name of the next unit
  388.     while ((aBytes >= 999.5) && (unitIndex < gStr.units.length - 1)) {
  389.       aBytes /= 1024;
  390.       unitIndex++;
  391.     }
  392.  
  393.     // Get rid of insignificant bits by truncating to 1 or 0 decimal points
  394.     // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235
  395.     aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) ? 1 : 0);
  396.  
  397.     return [aBytes, gStr.units[unitIndex]];
  398.   },
  399.  
  400.   /**
  401.    * Converts a number of seconds to the two largest units. Time values are
  402.    * whole numbers, and units have the correct plural/singular form.
  403.    *
  404.    * @param aSecs
  405.    *        Seconds to convert into the appropriate 2 units
  406.    * @return 4-item array [first value, its unit, second value, its unit]
  407.    */
  408.   convertTimeUnits: function(aSecs)
  409.   {
  410.     // These are the maximum values for seconds, minutes, hours corresponding
  411.     // with gStr.timeUnits without the last item
  412.     let timeSize = [60, 60, 24];
  413.  
  414.     let time = aSecs;
  415.     let scale = 1;
  416.     let unitIndex = 0;
  417.  
  418.     // Keep converting to the next unit while we have units left and the
  419.     // current one isn't the largest unit possible
  420.     while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) {
  421.       time /= timeSize[unitIndex];
  422.       scale *= timeSize[unitIndex];
  423.       unitIndex++;
  424.     }
  425.  
  426.     let value = convertTimeUnitsValue(time);
  427.     let units = convertTimeUnitsUnits(value, unitIndex);
  428.  
  429.     let extra = aSecs - value * scale;
  430.     let nextIndex = unitIndex - 1;
  431.  
  432.     // Convert the extra time to the next largest unit
  433.     for (let index = 0; index < nextIndex; index++)
  434.       extra /= timeSize[index];
  435.  
  436.     let value2 = convertTimeUnitsValue(extra);
  437.     let units2 = convertTimeUnitsUnits(value2, nextIndex);
  438.  
  439.     return [value, units, value2, units2];
  440.   },
  441. };
  442.  
  443. /**
  444.  * Private helper for convertTimeUnits that gets the display value of a time
  445.  *
  446.  * @param aTime
  447.  *        Time value for display
  448.  * @return An integer value for the time rounded down
  449.  */
  450. function convertTimeUnitsValue(aTime)
  451. {
  452.   return Math.floor(aTime);
  453. }
  454.  
  455. /**
  456.  * Private helper for convertTimeUnits that gets the display units of a time
  457.  *
  458.  * @param aTime
  459.  *        Time value for display
  460.  * @param aIndex
  461.  *        Index into gStr.timeUnits for the appropriate unit
  462.  * @return The appropriate plural form of the unit for the time
  463.  */
  464. function convertTimeUnitsUnits(aTime, aIndex)
  465. {
  466.   // Negative index would be an invalid unit, so just give empty
  467.   if (aIndex < 0)
  468.     return "";
  469.  
  470.   return PluralForm.get(aTime, gStr.timeUnits[aIndex]);
  471. }
  472.  
  473. /**
  474.  * Private helper function to replace a placeholder string with a real string
  475.  *
  476.  * @param aText
  477.  *        Source text containing placeholder (e.g., #1)
  478.  * @param aIndex
  479.  *        Index number of placeholder to replace
  480.  * @param aValue
  481.  *        New string to put in place of placeholder
  482.  * @return The string with placeholder replaced with the new string
  483.  */
  484. function replaceInsert(aText, aIndex, aValue)
  485. {
  486.   return aText.replace("#" + aIndex, aValue);
  487. }
  488.  
  489. /**
  490.  * Private helper function to determine if an argument is null or undefined
  491.  *
  492.  * @param aArg
  493.  *        The argument to check for nullness or undefinedness
  494.  * @return true if null or undefined, false otherwise
  495.  */
  496. function isNil(aArg)
  497. {
  498.   return (aArg == null) || (aArg == undefined);
  499. }
  500.  
  501. /**
  502.  * Private helper function to log errors to the error console and command line
  503.  *
  504.  * @param aMsg
  505.  *        Error message to log or an array of strings to concat
  506.  */
  507. function log(aMsg)
  508. {
  509.   let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg);
  510.   Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).
  511.     logStringMessage(msg);
  512.   dump(msg + "\n");
  513. }
  514.